/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.architexa.org.eclipse.gef.ui.parts;

import com.architexa.org.eclipse.draw2d.FigureCanvas;
import com.architexa.org.eclipse.draw2d.IFigure;
import com.architexa.org.eclipse.draw2d.PositionConstants;
import com.architexa.org.eclipse.draw2d.geometry.Point;
import com.architexa.org.eclipse.draw2d.geometry.Rectangle;
import com.architexa.org.eclipse.gef.ConnectionEditPart;
import com.architexa.org.eclipse.gef.EditPart;
import com.architexa.org.eclipse.gef.GraphicalEditPart;
import com.architexa.org.eclipse.gef.GraphicalViewer;
import com.architexa.org.eclipse.gef.KeyHandler;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;



/**
 * An extended KeyHandler which processes default keystrokes for common navigation in a
 * GraphicalViewer. This class can be used as a KeyHandler too; Unrecognized keystrokes
 * are sent to the super's implementation. This class will process key events containing
 * the following:
 * <UL>
 *   <LI>Arrow Keys (UP, DOWN, LEFT, RIGHT) with optional SHIFT and CONTROL modifiers
 *   <LI>Arrow Keys (UP, DOWN) same as above, but with ALT modifier.
 *   <LI>'\'Backslash and '/' Slash keys with optional SHIFT and CONTROL modifiers
 * </UL>
 * <P>All processed key events will do nothing other than change the selection and/or
 * focus editpart for the viewer.
 * @author hudsonr
 */
public class GraphicalViewerKeyHandler
	extends KeyHandler
{

int counter;

/**
 * When navigating through connections, a "Node" EditPart is used as a reference.
 */
private WeakReference cachedNode;
private GraphicalViewer viewer;

/**
 * Constructs a key handler for the given viewer.
 * @param viewer the viewer
 */
public GraphicalViewerKeyHandler(GraphicalViewer viewer) {
	this.viewer = viewer;
}

/**
 * @return	<code>true</code> if key pressed indicates a connection traversal/selection
 */
boolean acceptConnection(KeyEvent event) {
	return event.character == '/'
		|| event.character == '?'
		|| event.character == '\\'
		|| event.character == '\u001c'
		|| event.character == '|';
}

/**
 * @return	<code>true</code> if the keys pressed indicate to traverse inside a container
 */
boolean acceptIntoContainer(KeyEvent event) {
	return ((event.stateMask & SWT.ALT) != 0) && (event.keyCode == SWT.ARROW_DOWN);
}

/**
 * @return	<code>true</code> if the keys pressed indicate to stop traversing/selecting
 * 			connection
 */
boolean acceptLeaveConnection(KeyEvent event) {
	int key = event.keyCode;
	if (getFocusEditPart() instanceof ConnectionEditPart)
		if ((key == SWT.ARROW_UP)
		  || (key == SWT.ARROW_RIGHT)
		  || (key == SWT.ARROW_DOWN)
		  || (key == SWT.ARROW_LEFT))
			return true;
	return false;
}

/**
 * @return	<code>true</code> if the viewer's contents has focus and one of the arrow
 * 			keys is pressed
 */
boolean acceptLeaveContents(KeyEvent event) {
	int key = event.keyCode;
	return getFocusEditPart() == getViewer().getContents()
		&& ((key == SWT.ARROW_UP)
			|| (key == SWT.ARROW_RIGHT)
			|| (key == SWT.ARROW_DOWN)
			|| (key == SWT.ARROW_LEFT));
}

/**
 * @return	<code>true</code> if the keys pressed indicate to traverse to the parent of
 * 			the currently focused EditPart
 */
boolean acceptOutOf(KeyEvent event) {
	return ((event.stateMask & SWT.ALT) != 0) && (event.keyCode == SWT.ARROW_UP);
}

boolean acceptScroll(KeyEvent event) {
	return ((event.stateMask & SWT.CTRL) != 0 && (event.stateMask & SWT.SHIFT) != 0 
			&& (event.keyCode == SWT.ARROW_DOWN || event.keyCode == SWT.ARROW_LEFT
			|| event.keyCode == SWT.ARROW_RIGHT || event.keyCode == SWT.ARROW_UP));
}

/**
 * Given a connection on a node, this method finds the next (or the previous) connection
 * of that node.
 * 
 * @param	node	The EditPart whose connections are being traversed
 * @param	current	The connection relative to which the next connection has to be found
 * @param	forward	<code>true</code> if the next connection has to be found; false otherwise
 */
ConnectionEditPart findConnection(GraphicalEditPart node, 
                                            ConnectionEditPart current, boolean forward) {
	List connections = new ArrayList(node.getSourceConnections());
	connections.addAll(node.getTargetConnections());
	if (connections.isEmpty())
		return null;
	if (forward)
		counter++;
	else
		counter--;
	while (counter < 0)
		counter += connections.size();
	counter %= connections.size();
	return (ConnectionEditPart)connections.get(counter % connections.size());
}

/**
 * Given an absolute point (pStart) and a list of EditParts, this method finds the closest
 * EditPart (except for the one to be excluded) in the given direction.
 * 
 * @param	siblings	List of sibling EditParts
 * @param	pStart		The starting point (must be in absolute coordinates) from which
 * 						the next sibling is to be found.
 * @param	direction	PositionConstants
 * @param	exclude		The EditPart to be excluded from the search
 * 
 */
GraphicalEditPart findSibling(List siblings, Point pStart, int direction,
                                        EditPart exclude) {
	GraphicalEditPart epCurrent;
	GraphicalEditPart epFinal = null;
	IFigure figure;
	Point pCurrent;
	int distance = Integer.MAX_VALUE;

	Iterator iter = siblings.iterator();
	while (iter.hasNext()) {
		epCurrent = (GraphicalEditPart)iter.next();
		if (epCurrent == exclude || !epCurrent.isSelectable())
			continue;
		figure = epCurrent.getFigure();
		pCurrent = getNavigationPoint(figure);
		figure.translateToAbsolute(pCurrent);
		if (pStart.getPosition(pCurrent) != direction)
			continue;

		int d = pCurrent.getDistanceOrthogonal(pStart);
		if (d < distance) {
			distance = d;
			epFinal = epCurrent;
		}
	}
	return epFinal;
}

/**
 * Figures' navigation points are used to determine their direction compared to one 
 * another, and the distance between them.
 *  
 * @return	the center of the given figure
 */
Point getNavigationPoint(IFigure figure) {
	return figure.getBounds().getCenter();
}

/**
 * Returns the cached node. It is possible that the node is not longer in the viewer but
 * has not been garbage collected yet.
 */
private GraphicalEditPart getCachedNode() {
	if (cachedNode == null)
		return null;
	if (cachedNode.isEnqueued())
		return null;
	return (GraphicalEditPart)cachedNode.get();
}

/**
 * @return	the EditPart that has focus
 */
protected GraphicalEditPart getFocusEditPart() {
	return (GraphicalEditPart)getViewer().getFocusEditPart();
}

/**
 * Returns the list of editparts which are conceptually at the same level of navigation as
 * the currently focused editpart.  By default, these are the siblings of the focused 
 * part.
 * <p>
 * This implementation returns a list that contains the EditPart that has focus.
 * </p>
 * @return a list of navigation editparts
 */
List getNavigationSiblings() {
	EditPart focusPart = getFocusEditPart();
	if (focusPart.getParent() != null)
		return focusPart.getParent().getChildren();
	List list = new ArrayList();
	list.add(focusPart);
	return list;
}

/**
 * Returns the viewer on which this key handler was created.
 * @return the viewer
 */
protected GraphicalViewer getViewer() {
	return viewer;
}

/**
 * @return <code>true</code> if the viewer is mirrored
 * @since 3.1
 */
boolean isViewerMirrored() {
	return (viewer.getControl().getStyle() & SWT.MIRRORED) != 0;
}

/**
 * Extended to process key events described above.
 * @see com.architexa.org.eclipse.gef.KeyHandler#keyPressed(org.eclipse.swt.events.KeyEvent)
 */
public boolean keyPressed(KeyEvent event) {
	if (event.character == ' ') {
		processSelect(event);
		return true;
	} else if (acceptIntoContainer(event)) {
		navigateIntoContainer(event);
		return true;
	} else if (acceptOutOf(event)) {
		navigateOut(event);
		return true;
	} else if (acceptConnection(event)) {
		navigateConnections(event);
		return true;
	} else if (acceptScroll(event)) {
		scrollViewer(event);
		return true;
	} else if (acceptLeaveConnection(event)) {
		navigateOutOfConnection(event);
		return true;
	} else if (acceptLeaveContents(event)) {
		navigateIntoContainer(event);
		return true;
	}

	switch (event.keyCode) {
		case SWT.ARROW_LEFT:
			if (navigateNextSibling(event, isViewerMirrored() ? PositionConstants.EAST 
					: PositionConstants.WEST))
				return true;
			break;
		case SWT.ARROW_RIGHT:
			if (navigateNextSibling(event, isViewerMirrored() ? PositionConstants.WEST 
					: PositionConstants.EAST))
				return true;
			break;
		case SWT.ARROW_UP:
			if (navigateNextSibling(event, PositionConstants.NORTH))
				return true;
			break;
		case SWT.ARROW_DOWN:
			if (navigateNextSibling(event, PositionConstants.SOUTH))
				return true;
			break;

		case SWT.HOME:
			if (navigateJumpSibling(event, PositionConstants.WEST))
				return true;
			break;
		case SWT.END:
			if (navigateJumpSibling(event, PositionConstants.EAST))
				return true;
			break;
		case SWT.PAGE_DOWN:
			if (navigateJumpSibling(event, PositionConstants.SOUTH))
				return true;
			break;
		case SWT.PAGE_UP:
			if (navigateJumpSibling(event, PositionConstants.NORTH))
				return true;
	}
	return super.keyPressed(event);
}

/**
 * This method navigates through connections based on the keys pressed.
 */
void navigateConnections(KeyEvent event) {
	GraphicalEditPart focus = getFocusEditPart();
	ConnectionEditPart current = null;
	GraphicalEditPart node = getCachedNode();
	if (focus instanceof ConnectionEditPart) {
		current = (ConnectionEditPart)focus;
		if (node == null
		  || (node != current.getSource() && node != current.getTarget())) {
			node = (GraphicalEditPart)current.getSource();
			counter = 0;
		}
	} else {
		node = focus;
	}

	setCachedNode(node);
	boolean forward = event.character == '/'
		|| event.character == '?';
	ConnectionEditPart next = findConnection(node, current, forward);
	navigateTo(next, event);
}

/**
 * This method traverses to the closest child of the currently focused EditPart, if it has
 * one.
 */
void navigateIntoContainer(KeyEvent event) {
	GraphicalEditPart focus = getFocusEditPart();
	List childList = focus.getChildren();
	Point tl = focus.getContentPane().getBounds().getTopLeft();
	
	int minimum = Integer.MAX_VALUE;
	int current;
	GraphicalEditPart closestPart = null;

	for (int i = 0; i < childList.size(); i++) {	
		GraphicalEditPart ged = (GraphicalEditPart)childList.get(i);
		if (!ged.isSelectable())
			continue;
		Rectangle childBounds = ged.getFigure().getBounds();
		
		current = (childBounds.x - tl.x) + (childBounds.y - tl.y);
		if (current < minimum) {
			minimum = current;
			closestPart = ged;
		}
	}
	if (closestPart != null)
		navigateTo(closestPart, event);
}

/**
 * Not yet implemented.
 */
boolean navigateJumpSibling(KeyEvent event, int direction) {
	// TODO: Implement navigateJumpSibling() (for PGUP, PGDN, HOME and END key events)
	return false;
}

/**
 * Traverses to the next sibling in the given direction.
 * 
 * @param	event		the KeyEvent for the keys that were pressed to trigger this traversal
 * @param	direction	PositionConstants.* indicating the direction in which to traverse
 */
boolean navigateNextSibling(KeyEvent event, int direction) {
	return navigateNextSibling(event, direction, getNavigationSiblings());
}

/**
 * Traverses to the closest EditPart in the given list that is also in the given direction.
 * 
 * @param	event		the KeyEvent for the keys that were pressed to trigger this traversal
 * @param	direction	PositionConstants.* indicating the direction in which to traverse
 */
boolean navigateNextSibling(KeyEvent event, int direction, List list) {
	GraphicalEditPart epStart = getFocusEditPart();
	IFigure figure = epStart.getFigure();
	Point pStart = getNavigationPoint(figure);
	figure.translateToAbsolute(pStart);
	EditPart next = findSibling(list, pStart, direction, epStart);
	if (next == null)
		return false;
	navigateTo(next, event);
	return true;
}

/**
 * Navigates to the parent of the currently focused EditPart.
 */
void navigateOut(KeyEvent event) {
	if (getFocusEditPart() == null
		|| getFocusEditPart() == getViewer().getContents()
		|| getFocusEditPart().getParent() == getViewer().getContents())
		return;
	navigateTo(getFocusEditPart().getParent(), event);
}

/**
 * Navigates to the source or target of the currently focused ConnectionEditPart.
 */
void navigateOutOfConnection(KeyEvent event) {
	GraphicalEditPart cached = getCachedNode();
	ConnectionEditPart conn = (ConnectionEditPart)getFocusEditPart();
	if (cached != null
		&& (cached == conn.getSource()
			|| cached == conn.getTarget()))
		navigateTo(cached, event);
	else
		navigateTo(conn.getSource(), event);
}

/**
 * Navigates to the given EditPart
 * 
 * @param	part	the EditPart to navigate to
 * @param	event	the KeyEvent that triggered this traversal
 */
protected void navigateTo(EditPart part, KeyEvent event) {
	if (part == null)
		return;
	if ((event.stateMask & SWT.SHIFT) != 0) {
		getViewer().appendSelection(part);
		getViewer().setFocus(part);
	} else if ((event.stateMask & SWT.CONTROL) != 0)
		getViewer().setFocus(part);
	else
		getViewer().select(part);
	getViewer().reveal(part);
}

/**
 * This method is invoked when the user presses the space bar.  It toggles the selection
 * of the EditPart that currently has focus.
 * @param event the key event received
 */
    protected void processSelect(KeyEvent event) {
        EditPart part = getViewer().getFocusEditPart();
        if (part != getViewer().getContents()) {
            if ((event.stateMask & SWT.CONTROL) != 0
                && part.getSelected() != EditPart.SELECTED_NONE)
                getViewer().deselect(part);
            else
                getViewer().appendSelection(part);

            getViewer().setFocus(part);
        }
    }

void scrollViewer(KeyEvent event) {
	if (!(getViewer().getControl() instanceof FigureCanvas))
		return;
	FigureCanvas figCanvas = (FigureCanvas)getViewer().getControl();
	Point loc = figCanvas.getViewport().getViewLocation();
	Rectangle area = figCanvas.getViewport().getClientArea(Rectangle.SINGLETON).scale(.1); 
	switch (event.keyCode) {
		case SWT.ARROW_DOWN:
			figCanvas.scrollToY(loc.y + area.height);
			break;
		case SWT.ARROW_UP:
			figCanvas.scrollToY(loc.y - area.height);
			break;
		case SWT.ARROW_LEFT:
			if (isViewerMirrored())
				figCanvas.scrollToX(loc.x + area.width);
			else
				figCanvas.scrollToX(loc.x - area.width);
			break;
		case SWT.ARROW_RIGHT:
			if (isViewerMirrored())
				figCanvas.scrollToX(loc.x - area.width);
			else
				figCanvas.scrollToX(loc.x + area.width);
	}
}

private void setCachedNode(GraphicalEditPart node) {
	if (node == null)
		cachedNode = null;
	else 
		cachedNode = new WeakReference(node);
}

}
